Value Objects (VO) en PHP
Este es un concepto que conocí al adentrarme en lecturas de Domain Driven Design (concepto que introduce Eric Evans en su libro de nombre homónimo por el 2006), temática que en un futuro iré desglosando y procesando mediante posteos en forma progresiva.
El autor de este libro define el concepto de VO como:
Objects that matter only as the combination of their attributes. Two value objects with the same values for all their attributes are considered equal.
Por si te surje la duda mientras lees -como a mí al empezar con la redacción-, este patrón es pre-existente a la definición formal de DDD y solo es tomado como una herramienta estándar, dentro del "arsenal" que nos propone para solventar ciertos problemas.
Sujeto al párrafo anterior, el concepto de VO se encuentra expresado en otras palabras por otro conocido en el rubro como Martin Fowler que lo expone en su libro Patterns of Enterprise Application Architecture
A small simple object, like money or a date range, whose equality isn't based on identity.
Caracteristicas
De las definiciones ya expuestas de este patrón, subyacen algunas características relevantes, aunque procederé a enmarcar aquellas que sin estar presente en las definiciones (explicitamente) son relevantes:
Medir, Cuantificar o Describir: Los VO permiten medir, cuantificar o describir un concepto de nuestra capa de dominio en forma precisa. No se consideran algo, sino que son un valor, y como tal tienen un fin.
Invariante: Desde su instanciación un VO nos provee la seguridad de existir en un estado válido, ya que en caso de que alguna pre o pos condición no se cumpla (al construirlo o operar sobre él), lanzará una excepción.
Inmutabilidad: Una característica de las relevantes, ya que un VO es un objeto que durante todo su ciclo de vida nos asegura que no podrá ser modificado, es decir que cada cambio que se quiera realizar sobre el resulta en realidad en un nuevo objeto que fue inicializado en el estado deseado y resultante de la acción solicitada. Como efecto colateral esto nos asegura un sencillo razonamiento/entendimiento de su fin, que testear su comportamiento sea sencillo y la incapacidad de generar efectos colaterales.
Igualdad por estructura: Un VO es igual a otro cuando (son del mismo tipo -clase- y ) su estado/atributos son idénticos. El ejemplo más ampliamente usado, es el de dos persona que intercambian billetes de 5 USD entre sí, ya que a fines monetarios, su valor es el mismo y aunque el billete es otro, en significado lo que la persona tiene en propiedad no ha cambiado.
Extra: una buena práctica con los VO consiste en proveer constructores estáticos con mayor expresividad(semántica) que faciliten la creación de los mismos, especialmente cuando uno de los parámetros tiene un conjunto limitado de valores válidos (por ej. Enumerados al implementarse en PHP).
Del dicho al hecho
Aquí pretendo colocar ejemplos lo más terrenales posibles y vinculados a contextos reales en los que participé.
Comienzo con el aspecto de que un VO nos permite "medir, cuantificar o describir", para esto tomare de ejemplo el concepto de negocio "puerto" de un servidor, objeto reutilizable mediante otro de mayor porte y que lo contenga o utilice como puede ser, una configuración SMTP, o bien, un endpoint. Aclaro que todo endpoint aunque nosotros no le indiquemos el puerto (https://api.sandbox.paypal.com/v2/payments/authorizations/0VF52814937998046) será el 443 para https o el 80 si es http, es decir que siempre esta presente, aunque funcione camuflado para el usuario final.
<?php
class ServerPort {
private $port = 0;
public function __construct(int $port) {
if($port < 0 || $port > 65535) {
throw new Exception('A server port must be a number between 0 and 65.535');
}
$this->port = $port;
}
public function port() :int {
return $this->port;
}
public function __toString() :string {
// Php will execute for you the cast from int -> string.
return $this->port;
}
}
Esto nos permite "describir" en forma clara que es lo que el puerto de un servidor debe/puede ser y que no, generando una falla manejable(dado el proyecto, podría ser un tipo especifico de excepción, ej. DomainModelException
) y descriptiva.
Vinculando este ejemplo del ServerPort
con el tópico "extra" de constructores estáticos expresivos, adiciono al código anterior lo necesario para clarificar la idea, vinculando lo del uso de este VO en uno de tipo Endpoint
.
<?php
class ServerPort {
public const HTTP_PORT = 80;
public const HTTPS_PORT = 443;
private $port = 0;
public static function httpPort() :self {
return new self(self::HTTP_PORT);
}
public static function httpsPort() :self {
return new self(self::HTTPS_PORT);
}
public function __construct(int $port) {
if($port < 0 || $port > 65535) {
throw new Exception('A server port must be a number between 0 and 65.535');
}
$this->port = $port;
}
public function port() :int {
return $this->port;
}
public function __toString() :string {
// Php will execute for you the cast from int -> string.
return $this->port;
}
}
Si bien ya observamos el aspecto de la gestión de las "invariantes", ya que en el caso del ServerPort
se limita el rango entero de números seleccionable como puerto, avanzaremos con un ejemplo que lo profundice un poco más y nos presente la posibilidad de aplicar la "igualdad por estructura".
<?php
class EmailAddress {
private $name;
private $domain;
public static function httpPort() :self {
return new self(self::HTTP_PORT);
}
public static function httpsPort() :self {
return new self(self::HTTPS_PORT);
}
public function __construct(string $name, WebDomain $domain) {
$this->assignName($name);
$this->assignDomain($domain);
}
private assignName(string $name) :void {
if(empty($name)) {
throw new Exception('The email name can not be an empty string');
}
if(!preg_match("/[0-9a-zA-Z\.]+/", $name)) {
throw new Exception('The email name has not a valid format');
}
$this->name = $name;
}
private function assignDomain(WebDomain $domain) :void {
/*** Atención aquí ! ***/
$this->domain = $domain;
}
public function name() :string{
return $this->name;
}
public function domain() :string{
return $this->domain;
}
public function changeName(string $newName) :self {
return new self($newName, $this->domain);
}
public function __toString() :string {
return $this->name."@".$this->domain;
}
public function equals(self $otherEmailAddress) :bool {
return (
$this->name == $otherEmailAddress->name &&
$this->domain.equals($otherEmailAddress->domain)
);
}
}
Si bien en este ejemplo falta implementar el objeto WebDomain
, este sera similar el ServerPort
-visible como 1er ejemplo en este post-, con la variación de gestionar las validaciones pertinentes para que el estado esté conformado por un dominio web correcto para nuestro dominio/alcance.
En cuanto a las "invariantes" en el EmailAddress
son mantenidas en los métodos assignATRIBUTO, que a priori el assignDomain
se ve redundante ya que no aporta mucho valor, aunque el apartarlo a otro método nos permite en un futuro diferenciar o ampliar sus restricciones. Es que si para el WebDomain
el sitio blog.lucianothoma.xyz es válido, puede que para este contexto no, y queramos agregar la verificación de que solo sean dominios primarios (lucianothoma.xyz o gobierno.com.uy) y no subdominios, o especificamente de X proveedor/es (gmail.com y outlook.com).
El detalle faltante al ejemplo del ServerPort, es el hecho de implementar el método equals
para compararse con otra instancia de la misma clase donde se comparen por igualdad los valores del puerto de cada uno. Detalle que doy por comprendido con esta aclaración.
Continuando con el método equals
pero ahora en la clase EmailAddress
, vemos como los atributos de este que son valores primitivos (números, strings, booleanos y nulleables) se comparan con el operador de igualdad (podría ser por identidad también ===
), mientras que aquellos atributos que están representados vía otros VO los comparamos entre sí usando su método equals
. Aquí hemos dejado en código (posiblemente más claro) a que refiere esto de la "igualdad por estructura".
Aquí mismo se implementa la famosa "inmutabilidad" que básicamente refiere a que desde su creación todo VO no debe mutar y para lograr ese cambio, lo que en realidad se realiza es la creación de un nuevo objeto con esta característica modificada, como lo hago en el método changeName
.
Para finalizar, marco un detalle que puede que al leer se pase por alto:
- El método mágico
__toString
que es llamado siempre que un objeto se requiera como cadena de texto, en forma explicita (al indicarlo en el código (string) $variable ) o implícita (al concatenar un objecto con un string, o bien, al definir un tipo de retorno string en una función y simplemente devolver el objeto a secas).